/**
 * 
 */
package de.akabahn.ebd.fst.control;

import static de.akabahn.ebd.comm.Config.FSTCTRL_USER_FILE;
import static de.akabahn.ebd.comm.Config.FSTCTRL_CONF_FILE;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.Properties;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;

import de.akabahn.ebd.comm.Util;
import de.akabahn.ebd.fst.FahrSteuerungAdapter;
import de.akabahn.ebd.fst.FahrSteuerungAdapterFactory;

/**
 * Fahrsteuerung-Control application that checks that each Fahrsteuerung client
 * which runs a train via the smartphone app is alive and able to communicate
 * with the Fahrsteuerung. To ensure this, the clients have to sent ping
 * (keep-alive) messages to this application. If these messages are missing for
 * a configurable time, the trains that are controlled by the client are
 * stopped. This control application supports the following message types:
 * <ol>
 * <li>PING, keep-alive message</li>
 * <li>GO, inform the application that the client wants to move a tfz</li>
 * <li>STOP, inform the application that the clients wants to stop a tfz</li>
 * <li>AUTH, authenticate a user</li>
 * <li>NEWPWD, change the password for a user</li>
 * <li>TFZINFO, return Tfz information</li>
 * </ol>
 * The application should be started via the
 * <code>FahrSteuerungControlLauncher</code> application.
 * 
 * @author Peter Eimann
 * @since 1.0
 * @version 1.0
 * @see ClientConnectionHandler
 * @see FahrSteuerungControlLauncher
 */
public class FahrSteuerungControl implements Runnable, Observer {

	private final static Logger LOG = Logger.getLogger(FahrSteuerungControl.class);

	private int port;
	private int checkInterval;
	private boolean test;
	private String fstServer;
	private int fstPort;
	private Map<String, Client> clients;
	private ExecutorService threadPool;
	private ServerSocket serverSocket;
	private CountDownLatch pingLatch;
	private CountDownLatch exitLatch;
	private FahrSteuerungAdapter fst;
	private Map<String, List<String>> tfzMap;

	/**
	 * Initialize this instance
	 * 
	 * @param port
	 *            The TCP port where the application listens for incoming
	 *            requests
	 * @param checkInterval
	 *            The time in ms between each check if each client has sent a
	 *            new ping (keep-alive) request
	 * @param test
	 *            test mode flag, if set to <code>true</code>, no connection to
	 *            the Fahrsteuerung is established
	 * @param fstServer
	 *            ip-address or name of the Fahrsteuerung server (optional if
	 *            the default value shouldn't be used)
	 * @param fstPort
	 *            port of the Fahrsteuerung server (optional if the default
	 *            value shouldn't be used)
	 */
	public FahrSteuerungControl(int port, int checkInterval, boolean test, String fstServer, int fstPort) {
		this.port = port;
		this.checkInterval = checkInterval;
		this.test = test;
		this.fstServer = fstServer;
		this.fstPort = fstPort;

		clients = Collections.synchronizedMap(new HashMap<String, Client>());
		pingLatch = new CountDownLatch(1);
		exitLatch = new CountDownLatch(1);
		tfzMap = new HashMap<String, List<String>>();

		if (!this.test) {
			if (this.fstServer == null) {
				fst = FahrSteuerungAdapterFactory.getUncontrolledImplementation(this);
			} else {
				fst = FahrSteuerungAdapterFactory.getUncontrolledImplementation(this.fstServer, this.fstPort, this);
			}
		}
	}

	/**
	 * Start the control application by creating two threads:
	 * <ol>
	 * <li>A thread that controls the TCP/IP connections with the clients, using
	 * the <code>ClientConnectionHandler</code></li>
	 * <li>A thread that controls if the clients have sent their ping (keep
	 * alive) messages and that stops moving trains if these messages are
	 * missing</li>
	 * </ol>
	 * A shutdown hook is registered to close the server socket and the client
	 * connections when the application is terminated.
	 * 
	 * @throws IOException
	 * @see ClientConnectionHandler#run()
	 * @see #run()
	 * @see #parseTfzFeinwerte()
	 */
	public void start() throws IOException {
		threadPool = Executors.newCachedThreadPool();

		if (!test) {
			fst.start();
			LOG.info("FahrSteuerungAdapter started");
		}

		LOG.info("Parsing Tfz-Feinwerte File");
		parseTfzFeinwerte();

		serverSocket = new ServerSocket(port);
		LOG.info("Connection opened on port: " + port);

		Thread connectionHandlerThread = new Thread(new ClientConnectionHandler(this), "ClientConnectionHandler");
		LOG.info("Starting ClientConnectionHandler");
		connectionHandlerThread.start();

		Thread pingHandler = new Thread(this, "PingHandler");
		LOG.info("Starting PingHandler");
		pingHandler.start();

		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				LOG.info("Shutting down the thread pool, <STRG>+C received");
				threadPool.shutdown();

				try {
					threadPool.awaitTermination(5L, TimeUnit.SECONDS);

					if (!serverSocket.isClosed()) {
						serverSocket.close();
					}
				} catch (IOException e) {
				} catch (InterruptedException e) {
				}

				if (!test) {
					fst.stop();
					LOG.info("FahrSteuerungAdapter stopped");
				}

				LOG.info("Shutdown finished");
				exitLatch.countDown();
				pingLatch.countDown();
			}
		});

		while (!Util.waitForLatch(exitLatch, 5000));
	}

	/**
	 * The TCP port where the application listens for incoming client requests
	 * 
	 * @return The TCP port
	 */
	public int getPort() {
		return port;
	}

	/**
	 * The time in ms between each check if each client has sent a new ping
	 * (keep-alive) request
	 * 
	 * @return The check interval time
	 */
	public int getCheckInterval() {
		return checkInterval;
	}

	/**
	 * Test-Mode enabled or not
	 * 
	 * @return The test mode
	 */
	public boolean isTest() {
		return test;
	}

	/**
	 * Ip-address or name of the Fahrsteuerung server
	 * 
	 * @return The Fahrsteuerung server
	 */
	public String getFstServer() {
		return fstServer;
	}

	/**
	 * Port of the Fahrsteuerung server
	 * 
	 * @return The Fahrsteuerung port
	 */
	public int getFstPort() {
		return fstPort;
	}

	/**
	 * The clients that are currently connected to this application. Each client
	 * is identified by its client-id, which is the client's IP-address.
	 * 
	 * @return the clients
	 */
	public Map<String, Client> getClients() {
		return clients;
	}

	/**
	 * @return the tfzMap
	 */
	public Map<String, List<String>> getTfzMap() {
		return tfzMap;
	}

	/**
	 * Return the thread execution pool
	 * 
	 * @return the threadPool
	 */
	public ExecutorService getThreadPool() {
		return threadPool;
	}

	/**
	 * Return the server socket where the client connections are accepted
	 * 
	 * @return the serverSocket
	 */
	public ServerSocket getServerSocket() {
		return serverSocket;
	}

	/**
	 * Load the configuration file <code>conf/config.properties</code>
	 * 
	 * @return The configuration file
	 */
	public Properties loadConfigProperties() {
		Properties p = new Properties();
		try (FileInputStream fis = new FileInputStream(FSTCTRL_CONF_FILE)) {
			p.load(fis);
		} catch (Exception e) {
			LOG.error("Error loading configuration property file", e);
		}

		return p;
	}

	/**
	 * Load the configuration file <code>conf/user.properties</code>
	 * 
	 * @return The configuration file
	 */
	public Properties loadUserProperties() {
		Properties p = new Properties();
		try (FileInputStream fis = new FileInputStream(FSTCTRL_USER_FILE)) {
			p.load(fis);
		} catch (Exception e) {
			LOG.error("Error loading user property file", e);
		}

		return p;
	}

	/**
	 * Save the configuration file <code>conf/user.properties</code>
	 * 
	 * @param p The configuration file
	 * @param comment The comment that is written to the file
	 * @return
	 */
	public synchronized boolean saveUserProperties(Properties p, String comment) {
		try (FileOutputStream fos = new FileOutputStream(FSTCTRL_USER_FILE)) {
			p.store(fos, comment);
		} catch (Exception e) {
			LOG.error("Error writing config file", e);
			return false;
		}

		return true;
	}

	/**
	 * The loop that checks if the connected clients which currently run a Tfz
	 * are sending their regular ping (keep-alive) packets. If the time
	 * difference between the last ping message and now is greater than the
	 * check interval, the controlled Tfz are stopped.
	 */
	@Override
	public void run() {
		Client client = null;
		Set<String> tfzSet = null;
		List<String> clientsToRemove = new ArrayList<String>();

		do {
			Long now = System.currentTimeMillis();

			for (String clientID : clients.keySet()) {
				client = clients.get(clientID);
				tfzSet = client.getTfzAddressSet();

				if (!tfzSet.isEmpty() && ((now - client.getLastPing()) > checkInterval)) {
					for (String tfz : tfzSet) {
						LOG.warn("Ping exceeded for Tfz " + tfz);
						if (!test) {
							fst.stop(tfz);
							LOG.warn("Tfz " + tfz + " stopped");
						}
					}

					clientsToRemove.add(clientID);
				}
			}

			if (!clientsToRemove.isEmpty()) {
				for (String clientToRemove : clientsToRemove) {
					clients.remove(clientToRemove);
				}

				clientsToRemove.clear();
			}

			if (!clients.isEmpty()) {
				LOG.debug("Now = " + now + ", Clients = " + clients);
			}

		} while (!Util.waitForLatch(pingLatch, checkInterval));

		LOG.info("PingHandler finished");
	}

	/**
	 * @see java.util.Observer#update(java.util.Observable, java.lang.Object)
	 */
	public void update(Observable observable, Object object) {
		LOG.info("Connected to Fahrsteuerung: " + object.toString());
	}

	/**
	 * Parse the file that contains the Tfz information values (speed map,
	 * special functions) and store the data in an internal map with the Tfz
	 * number as key. Each line in this file is a tab separated list in the
	 * following format:
	 * <code><Tfz-number> &lt;V5&gt; &lt;V10&gt; &lt;V15&gt;.....&lt;V240&gt; TYP HG PFEI TUER ABV</code>
	 * The location of the file is configured using the key
	 * <code>tfzFeinwerteFile</code> in the configuration file.
	 */
	private void parseTfzFeinwerte() {
		Path tfzFile = Paths.get(loadConfigProperties().getProperty("tfzFeinwerteFile"));
		String line = null;
		List<String> tfzConfig = null;

		try (Scanner scanner = new Scanner(tfzFile)) {
			while (scanner.hasNextLine()) {
				line = scanner.nextLine();
				if (!"".equals(line.trim()) && !line.startsWith("#")) {
					tfzConfig = new ArrayList<String>();
					String[] values = line.split("\\t");
					// Index 0 = Tfz-Nummer
					for (int index = 1; index < values.length; index++) {
						tfzConfig.add(values[index]);
					}
					tfzMap.put(values[0], tfzConfig);
				}
			}
		} catch (IOException e) {
			LOG.warn("Error parsing tfz configuration file", e);
		}
	}
}
